/* *
 *
 *  (c) 2010-2019 Torstein Honsi
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "./Globals.js";
import U from "./Utilities.js";
var attr = U.attr,
  defined = U.defined,
  extend = U.extend,
  pick = U.pick,
  pInt = U.pInt;
import "./SvgRenderer.js";
var createElement = H.createElement,
  css = H.css,
  isFirefox = H.isFirefox,
  isMS = H.isMS,
  isWebKit = H.isWebKit,
  SVGElement = H.SVGElement,
  SVGRenderer = H.SVGRenderer,
  win = H.win;
/* eslint-disable valid-jsdoc */
// Extend SvgElement for useHTML option.
extend(
  SVGElement.prototype,
  /** @lends SVGElement.prototype */ {
    /**
     * Apply CSS to HTML elements. This is used in text within SVG rendering and
     * by the VML renderer
     *
     * @private
     * @function Highcharts.SVGElement#htmlCss
     *
     * @param {Highcharts.CSSObject} styles
     *
     * @return {Highcharts.SVGElement}
     */
    htmlCss: function (styles) {
      var wrapper = this,
        element = wrapper.element,
        // When setting or unsetting the width style, we need to update
        // transform (#8809)
        isSettingWidth =
          element.tagName === "SPAN" && styles && "width" in styles,
        textWidth = pick(isSettingWidth && styles.width, void 0),
        doTransform;
      if (isSettingWidth) {
        delete styles.width;
        wrapper.textWidth = textWidth;
        doTransform = true;
      }
      if (styles && styles.textOverflow === "ellipsis") {
        styles.whiteSpace = "nowrap";
        styles.overflow = "hidden";
      }
      wrapper.styles = extend(wrapper.styles, styles);
      css(wrapper.element, styles);
      // Now that all styles are applied, to the transform
      if (doTransform) {
        wrapper.htmlUpdateTransform();
      }
      return wrapper;
    },
    /**
     * VML and useHTML method for calculating the bounding box based on offsets.
     *
     * @private
     * @function Highcharts.SVGElement#htmlGetBBox
     *
     * @param {boolean} refresh
     *        Whether to force a fresh value from the DOM or to use the cached
     *        value.
     *
     * @return {Highcharts.BBoxObject}
     *         A hash containing values for x, y, width and height.
     */
    htmlGetBBox: function () {
      var wrapper = this,
        element = wrapper.element;
      return {
        x: element.offsetLeft,
        y: element.offsetTop,
        width: element.offsetWidth,
        height: element.offsetHeight,
      };
    },
    /**
     * VML override private method to update elements based on internal
     * properties based on SVG transform.
     *
     * @private
     * @function Highcharts.SVGElement#htmlUpdateTransform
     * @return {void}
     */
    htmlUpdateTransform: function () {
      // aligning non added elements is expensive
      if (!this.added) {
        this.alignOnAdd = true;
        return;
      }
      var wrapper = this,
        renderer = wrapper.renderer,
        elem = wrapper.element,
        translateX = wrapper.translateX || 0,
        translateY = wrapper.translateY || 0,
        x = wrapper.x || 0,
        y = wrapper.y || 0,
        align = wrapper.textAlign || "left",
        alignCorrection = {
          left: 0,
          center: 0.5,
          right: 1,
        }[align],
        styles = wrapper.styles,
        whiteSpace = styles && styles.whiteSpace;
      /**
       * @private
       * @return {number}
       */
      function getTextPxLength() {
        // Reset multiline/ellipsis in order to read width (#4928,
        // #5417)
        css(elem, {
          width: "",
          whiteSpace: whiteSpace || "nowrap",
        });
        return elem.offsetWidth;
      }
      // apply translate
      css(elem, {
        marginLeft: translateX,
        marginTop: translateY,
      });
      if (!renderer.styledMode && wrapper.shadows) {
        // used in labels/tooltip
        wrapper.shadows.forEach(function (shadow) {
          css(shadow, {
            marginLeft: translateX + 1,
            marginTop: translateY + 1,
          });
        });
      }
      // apply inversion
      if (wrapper.inverted) {
        // wrapper is a group
        [].forEach.call(elem.childNodes, function (child) {
          renderer.invertChild(child, elem);
        });
      }
      if (elem.tagName === "SPAN") {
        var rotation = wrapper.rotation,
          baseline,
          textWidth = wrapper.textWidth && pInt(wrapper.textWidth),
          currentTextTransform = [
            rotation,
            align,
            elem.innerHTML,
            wrapper.textWidth,
            wrapper.textAlign,
          ].join(",");
        // Update textWidth. Use the memoized textPxLength if possible, to
        // avoid the getTextPxLength function using elem.offsetWidth.
        // Calling offsetWidth affects rendering time as it forces layout
        // (#7656).
        if (
          textWidth !== wrapper.oldTextWidth &&
          (textWidth > wrapper.oldTextWidth ||
            (wrapper.textPxLength || getTextPxLength()) > textWidth) &&
          // Only set the width if the text is able to word-wrap, or
          // text-overflow is ellipsis (#9537)
          (/[ \-]/.test(elem.textContent || elem.innerText) ||
            elem.style.textOverflow === "ellipsis")
        ) {
          // #983, #1254
          css(elem, {
            width: textWidth + "px",
            display: "block",
            whiteSpace: whiteSpace || "normal", // #3331
          });
          wrapper.oldTextWidth = textWidth;
          wrapper.hasBoxWidthChanged = true; // #8159
        } else {
          wrapper.hasBoxWidthChanged = false; // #8159
        }
        // Do the calculations and DOM access only if properties changed
        if (currentTextTransform !== wrapper.cTT) {
          baseline = renderer.fontMetrics(elem.style.fontSize, elem).b;
          // Renderer specific handling of span rotation, but only if we
          // have something to update.
          if (
            defined(rotation) &&
            (rotation !== (wrapper.oldRotation || 0) ||
              align !== wrapper.oldAlign)
          ) {
            wrapper.setSpanRotation(rotation, alignCorrection, baseline);
          }
          wrapper.getSpanCorrection(
            // Avoid elem.offsetWidth if we can, it affects rendering
            // time heavily (#7656)
            (!defined(rotation) && wrapper.textPxLength) || // #7920
              elem.offsetWidth,
            baseline,
            alignCorrection,
            rotation,
            align
          );
        }
        // apply position with correction
        css(elem, {
          left: x + (wrapper.xCorr || 0) + "px",
          top: y + (wrapper.yCorr || 0) + "px",
        });
        // record current text transform
        wrapper.cTT = currentTextTransform;
        wrapper.oldRotation = rotation;
        wrapper.oldAlign = align;
      }
    },
    /**
     * Set the rotation of an individual HTML span.
     *
     * @private
     * @function Highcharts.SVGElement#setSpanRotation
     * @param {number} rotation
     * @param {number} alignCorrection
     * @param {number} baseline
     * @return {void}
     */
    setSpanRotation: function (rotation, alignCorrection, baseline) {
      var rotationStyle = {},
        cssTransformKey = this.renderer.getTransformKey();
      rotationStyle[cssTransformKey] = rotationStyle.transform =
        "rotate(" + rotation + "deg)";
      rotationStyle[cssTransformKey + (isFirefox ? "Origin" : "-origin")] =
        rotationStyle.transformOrigin =
          alignCorrection * 100 + "% " + baseline + "px";
      css(this.element, rotationStyle);
    },
    /**
     * Get the correction in X and Y positioning as the element is rotated.
     *
     * @private
     * @function Highcharts.SVGElement#getSpanCorrection
     * @param {number} width
     * @param {number} baseline
     * @param {number} alignCorrection
     * @return {void}
     */
    getSpanCorrection: function (width, baseline, alignCorrection) {
      this.xCorr = -width * alignCorrection;
      this.yCorr = -baseline;
    },
  }
);
// Extend SvgRenderer for useHTML option.
extend(
  SVGRenderer.prototype,
  /** @lends SVGRenderer.prototype */ {
    /**
     * @private
     * @function Highcharts.SVGRenderer#getTransformKey
     *
     * @return {string}
     */
    getTransformKey: function () {
      return isMS && !/Edge/.test(win.navigator.userAgent)
        ? "-ms-transform"
        : isWebKit
        ? "-webkit-transform"
        : isFirefox
        ? "MozTransform"
        : win.opera
        ? "-o-transform"
        : "";
    },
    /**
     * Create HTML text node. This is used by the VML renderer as well as the
     * SVG renderer through the useHTML option.
     *
     * @private
     * @function Highcharts.SVGRenderer#html
     *
     * @param {string} str
     *        The text of (subset) HTML to draw.
     *
     * @param {number} x
     *        The x position of the text's lower left corner.
     *
     * @param {number} y
     *        The y position of the text's lower left corner.
     *
     * @return {Highcharts.HTMLDOMElement}
     */
    html: function (str, x, y) {
      var wrapper = this.createElement("span"),
        element = wrapper.element,
        renderer = wrapper.renderer,
        isSVG = renderer.isSVG,
        addSetters = function (gWrapper, style) {
          // These properties are set as attributes on the SVG group, and
          // as identical CSS properties on the div. (#3542)
          ["opacity", "visibility"].forEach(function (prop) {
            gWrapper[prop + "Setter"] = function (value, key, elem) {
              var styleObject = gWrapper.div ? gWrapper.div.style : style;
              SVGElement.prototype[prop + "Setter"].call(
                this,
                value,
                key,
                elem
              );
              if (styleObject) {
                styleObject[key] = value;
              }
            };
          });
          gWrapper.addedSetters = true;
        };
      // Text setter
      wrapper.textSetter = function (value) {
        if (value !== element.innerHTML) {
          delete this.bBox;
          delete this.oldTextWidth;
        }
        this.textStr = value;
        element.innerHTML = pick(value, "");
        wrapper.doTransform = true;
      };
      // Add setters for the element itself (#4938)
      if (isSVG) {
        // #4938, only for HTML within SVG
        addSetters(wrapper, wrapper.element.style);
      }
      // Various setters which rely on update transform
      wrapper.xSetter =
        wrapper.ySetter =
        wrapper.alignSetter =
        wrapper.rotationSetter =
          function (value, key) {
            if (key === "align") {
              // Do not overwrite the SVGElement.align method. Same as VML.
              key = "textAlign";
            }
            wrapper[key] = value;
            wrapper.doTransform = true;
          };
      // Runs at the end of .attr()
      wrapper.afterSetters = function () {
        // Update transform. Do this outside the loop to prevent redundant
        // updating for batch setting of attributes.
        if (this.doTransform) {
          this.htmlUpdateTransform();
          this.doTransform = false;
        }
      };
      // Set the default attributes
      wrapper
        .attr({
          text: str,
          x: Math.round(x),
          y: Math.round(y),
        })
        .css({
          position: "absolute",
        });
      if (!renderer.styledMode) {
        wrapper.css({
          fontFamily: this.style.fontFamily,
          fontSize: this.style.fontSize,
        });
      }
      // Keep the whiteSpace style outside the wrapper.styles collection
      element.style.whiteSpace = "nowrap";
      // Use the HTML specific .css method
      wrapper.css = wrapper.htmlCss;
      // This is specific for HTML within SVG
      if (isSVG) {
        wrapper.add = function (svgGroupWrapper) {
          var htmlGroup,
            container = renderer.box.parentNode,
            parentGroup,
            parents = [];
          this.parentGroup = svgGroupWrapper;
          // Create a mock group to hold the HTML elements
          if (svgGroupWrapper) {
            htmlGroup = svgGroupWrapper.div;
            if (!htmlGroup) {
              // Read the parent chain into an array and read from top
              // down
              parentGroup = svgGroupWrapper;
              while (parentGroup) {
                parents.push(parentGroup);
                // Move up to the next parent group
                parentGroup = parentGroup.parentGroup;
              }
              // Ensure dynamically updating position when any parent
              // is translated
              parents.reverse().forEach(function (parentGroup) {
                var htmlGroupStyle,
                  cls = attr(parentGroup.element, "class");
                /**
                 * Common translate setter for X and Y on the HTML
                 * group. Reverted the fix for #6957 du to
                 * positioning problems and offline export (#7254,
                 * #7280, #7529)
                 * @private
                 * @param {*} value
                 * @param {string} key
                 * @return {void}
                 */
                function translateSetter(value, key) {
                  parentGroup[key] = value;
                  if (key === "translateX") {
                    htmlGroupStyle.left = value + "px";
                  } else {
                    htmlGroupStyle.top = value + "px";
                  }
                  parentGroup.doTransform = true;
                }
                // Create a HTML div and append it to the parent div
                // to emulate the SVG group structure
                htmlGroup = parentGroup.div =
                  parentGroup.div ||
                  createElement(
                    "div",
                    cls ? { className: cls } : void 0,
                    {
                      position: "absolute",
                      left: (parentGroup.translateX || 0) + "px",
                      top: (parentGroup.translateY || 0) + "px",
                      display: parentGroup.display,
                      opacity: parentGroup.opacity,
                      pointerEvents:
                        parentGroup.styles && parentGroup.styles.pointerEvents, // #5595
                      // the top group is appended to container
                    },
                    htmlGroup || container
                  );
                // Shortcut
                htmlGroupStyle = htmlGroup.style;
                // Set listeners to update the HTML div's position
                // whenever the SVG group position is changed.
                extend(parentGroup, {
                  // (#7287) Pass htmlGroup to use
                  // the related group
                  classSetter: (function (htmlGroup) {
                    return function (value) {
                      this.element.setAttribute("class", value);
                      htmlGroup.className = value;
                    };
                  })(htmlGroup),
                  on: function () {
                    if (parents[0].div) {
                      // #6418
                      wrapper.on.apply({ element: parents[0].div }, arguments);
                    }
                    return parentGroup;
                  },
                  translateXSetter: translateSetter,
                  translateYSetter: translateSetter,
                });
                if (!parentGroup.addedSetters) {
                  addSetters(parentGroup);
                }
              });
            }
          } else {
            htmlGroup = container;
          }
          htmlGroup.appendChild(element);
          // Shared with VML:
          wrapper.added = true;
          if (wrapper.alignOnAdd) {
            wrapper.htmlUpdateTransform();
          }
          return wrapper;
        };
      }
      return wrapper;
    },
  }
);
